<?php

namespace PhpYes\Entity;

use PhpYes\Yes;
use Wgx\PdoEx\QueryBuilder;
use Wgx\PdoEx\QueryBuilderResult;

/**
 * 通用说明:
 *
 * 1. 写数据库的函数如果是 失败返回 false 的话, 那么只能是在执行 sql 失败后才能返回 false, 其它情况下都不应该返回 false,
 * 以保证函数被用在事务中时, 判断 执行 sql 失败 就回滚事务, 否则就继续 事务的行为能保持正确.
 *
 * 2. 当实现将 传入参数 切成一段段的来执行 写数据库 操作时, 只要一遇到 sql 执行失败就应该返回 false, 以保证整个函数在事务中行为的正确性.
 *
 * 3. 缓存设计了 2级 缓存, php cache 和 缓存
 * 3.1 php cache 是 php 将数据存储在 php 中.
 * 3.2 缓存 是 redis 或 memcache 这类内存型数据存储, 需要中心化部署, 就算是分布式部署的, 也需要数据是中心化的, 因为程序需要维护这个缓存中的数据和数据库中的数据进行同步.
 * 3.3 防冲击类型的缓存可以多实例部署, 每台运行 php 的机器都部署一个实例都行, 这类缓存允许数据有一定延时, 一般设计在 controller 层, 缓存着接口的结果数据,
 * 例如列表数据, 当请求列表接口时, 缓存有数据就直接返回(就算数据有些不准确了也可以容忍), 缓存没有数据就计算出列表数据, 存入缓存, 然后返回列表数据, 当然详情页一般是不会缓存的.
 * 我们这里的缓存不是这种类型, 我们这里的缓存需要程序维护缓存中的数据和数据库中的数据进行同步, 所以需要数据是中心化的, 我们这里的缓存也可以称为 内存数据库.
 *
 * 4. php cache
 * 4.1 需要存数据到缓存时 : 只能是 php cache 开启时 才能存数据到 php cache, 因为 关闭 php cache 就是为了避免保存大量不太会被复用的数据, 导致 php 无法释放这些数据占用的内存, 损害系统性能.
 * 4.2 需要从缓存取数据时 : 无论 php cache 是 开启 或者 关闭, 都要尝试从 php cache 取数据, 因为 php cache 可能之前开启过, 里面有数据, 尝试从里面取数据, 可以最大程度复用 php cache 中的数据, 提升性能.
 * 4.3 需要清理缓存数据时 : 无论 php cache 是 开启 或者 关闭, 都要尝试清理 php cache 数据, 因为 php cache 可能之前开启过, 里面有数据, 尝试清理缓存数据, 可以最大程度保证 php cache 中没有过期数据.
 *
 * 5. id缓存存取策略
 * 5.1 当判定只使用 id 取数据时(即除了 id 就没有任何其它条件), 就可以从缓存取数据 (请参见 getCachedValueById 中的实现逻辑)
 * 5.1.1 从 php cache 中取数据(无论 php cache 是否开启)
 * 5.1.2 如果开启了缓存, php cache 中没有的数据再从 缓存 中取, 如果开启了 php cache, 从缓存中取到的数据就会放入 php cache
 * 5.1.3 php cache 和 缓存 中都没有的数据最后再从 数据库 中取
 * 5.2 当判定使用 * 从数据库取数据时(即可以取到表的全部字段), 就可以将取到的数据存入缓存 (请参见 setIdCacheValue 或 setIdCacheValues 中的实现逻辑)
 * 5.2.1 如果开启了 php cache, 就将取到的数据放入 php cache
 * 5.2.2 如果开启了 缓存, 就将取到的数据放入 缓存
 *
 * 6. ORM 中的 select 函数 会使用 CRUD 中的 select 函数 来执行 SELECT,
 * 所以 ORM 中的 select 函数 不用关心 缓存存取策略,
 * ORM 中的 select 函数 使用的 缓存存取策略 就是 它自己使用的 CRUD 中的 select 函数的 缓存存取策略.
 *
 * 7. 如果开启了缓存, 以下函数执行写数据库操作成功后 会清理一下这个 [[ 表 ]] 的所有缓存
 * insertOrUpdate : 函数内无法确定是 INSERT 还是 UPDATE, 也无法确定缓存的键
 * insertMultiValueOrUpdate : 函数内无法确定是 INSERT 还是 UPDATE, 也无法确定缓存的键
 * insertValuesOrUpdate : 函数内无法确定是 INSERT 还是 UPDATE, 也无法确定缓存的键
 * update : 函数内无法确定缓存的键
 * updateIn($inColumn != 'id') : 函数内无法确定缓存的键
 * delete : 函数内无法确定缓存的键
 * deleteIn($inColumn != 'id') : 函数内无法确定缓存的键
 * 以上这些函数都无法确定缓存的键, 所以只能在执行成功后 清理一下这个 [[ 表 ]] 的所有缓存,
 * 这样一来, 对缓存的误伤就很大, 所以, 请慎用这些函数.
 *
 * 8. 如果开启了缓存，以下函数执行写数据库操作成功后 会清理一下这个对象的所有缓存(调用 clearCache() 实现),
 * updateToDb : 更新到数据库
 * deleteFromDb : 从数据库删除
 *
 * 9. 如果开启了缓存，以下函数执行写数据库操作成功后 会清理一下这个对象的基于id的缓存
 * updateMultiValue (执行更新前清理缓存) : 无法感知到除id之外的其它 缓存键字段
 * updateValues (执行更新前清理缓存) : 无法感知到除id之外的其它 缓存键字段
 * updateById : 无法感知到除id之外的其它 缓存键字段
 * updateIn($inColumn == 'id') : 无法感知到除id之外的其它 缓存键字段
 * updateByIds : 无法感知到除id之外的其它 缓存键字段
 * deleteById : 无法感知到除id之外的其它 缓存键字段
 * deleteIn($inColumn == 'id') : 无法感知到除id之外的其它 缓存键字段
 * deleteByIds : 无法感知到除id之外的其它 缓存键字段
 * 如果子类实现了其它形式的缓存逻辑, 以上这些函数都无法清理这些其它形式的缓存, 使用时请注意这点.
 *
 */
abstract class BaseEntity
{
    // region ========== Property ==========

    const DataTypeBool = 1;
    const DataTypeInt = 2;
    const DataTypeFloat = 3;
    const DataTypeString = 4;

    /** @var array */
    private $data;
    /** @var array */
    private $changeLog;

    /** @var null|QueryBuilder */
    private $qb;
    /** @var null|QueryBuilderResult */
    private $qbResult;

    // endregion

    // region ========== Construct ==========

    /**
     * @return null|static
     */
    public static function getSingle()
    {
        return Yes::getSingle(static::class);
    }

    /**
     * 子类可以重写，如果没有传递 $data，可以给予一个默认值
     * 如果默认值生成太耗性能，可以在子类中重写 newObjectDefault 设置默认值
     *
     * BaseEntity constructor.
     * @param null|array $data
     */
    public function __construct($data = null)
    {
        $this->data = $data;
        if (empty($data['id'])) {
            $this->changeLog = $data;
        }
    }

    /**
     * 其实子类可以重写构造函数来在没有传递 $data 时生成一个默认值
     * 但是生成默认值可能很耗性能，重写构造函数来生成默认值就不太合适
     * 所以这里提供一个单独的函数来做这个事情
     * 生成默认值，然后使用生成的默认值来生成一个新对象
     *
     * 注意: 默认 throw Exception 来封掉这个函数
     *
     * @return static
     * @throws \Exception
     */
    public function newObjectDefault()
    {
        throw new \Exception('请子类重写这个函数');
    }

    // endregion

    // region ========== Getter ==========

    /**
     * @return null|QueryBuilder
     */
    public function getQb()
    {
        if (empty($this->qb)) {
            $this->qb = QueryBuilder::useDb($this->getConnName());
        }
        return $this->qb;
    }

    /**
     * @return null|QueryBuilderResult
     */
    public function getQbResult()
    {
        return $this->qbResult;
    }

    /**
     * @return string
     */
    public function getSql()
    {
        return empty($this->qbResult) ? '' : $this->qbResult->getSql();
    }

    // endregion

    // region ========== Util ==========

    /**
     * 从 $id 中提取出 有效 的 id, 默认有效的id为 int 且 >0
     * 成功: return 提取出的 有效 id,
     * 失败: return false
     *
     * Warning :
     * This function may return Boolean false, but may also return a non-Boolean value which evaluates to false.
     * Please read the section on Booleans for more information.
     * Use the === operator for testing the return value of this function.
     *
     * @param mixed $id
     * @return false|mixed
     */
    public function validId($id)
    {
        $id = empty($id) ? 0 : intval($id);
        if ($id > 0) {
            return $id;
        }
        return false;
    }

    /**
     * 从 $ids 中提取出 有效且去重 后的 ids, 默认有效的id为 int 且 >0
     * @param array $ids
     * @return array
     */
    public function validIds($ids)
    {
        if (empty($ids)) {
            return array();
        }
        $vIds = array();
        foreach ($ids as $id) {
            // 调用函数 validId 可以把 "提取有效id" 的行为都统一到 validId,
            // 但性能不高, 而且需要子类重写 validId 的情况也不多,
            // 当遇到 需要子类 重写 validId 时, 同时把 validIds 也重写一下 就可以了.

            // $id = $this->validId($id);
            // if ($id !== false) {
            //     $vIds[$id] = 1;
            // }

            $id = empty($id) ? 0 : intval($id);
            if ($id > 0) {
                $vIds[$id] = 1;
            }
        }
        if (empty($vIds)) {
            return array();
        }
        return array_keys($vIds);
    }

    /**
     * 请参见 QueryBuilder::quoteColumn
     * @param string[] $columns
     * @return array|mixed
     */
    public function quoteColumn($columns)
    {
        if (empty($columns) || !is_array($columns)) {
            return $columns;
        }

        $column2quotedMap = $this->getColumn2QuotedMap();
        if (empty($column2quotedMap)) {
            return $columns;
        }

        return QueryBuilder::quoteColumn($columns, $column2quotedMap);
    }

    /**
     * 请参见 QueryBuilder::quoteColumnK
     * @param array $kv
     * @return array|mixed
     */
    public function quoteColumnK($kv)
    {
        if (empty($kv) || !is_array($kv)) {
            return $kv;
        }

        $column2quotedMap = $this->getColumn2QuotedMap();
        if (empty($column2quotedMap)) {
            return $kv;
        }

        return QueryBuilder::quoteColumnK($kv, $column2quotedMap);
    }

    // endregion

    // region ========== Setting ==========

    /**
     * 主库连接名, 用来获取数据库连接
     * 连接名 被设计成 不同数据库使用不同的 连接名
     * 但相同的数据库使用不同的用户名(不同的用户名可能被授予的数据库操作权限不同)和密码或不同的其它连接属性时可以使用 不同 的连接名
     *
     * @return string
     */
    abstract public function getConnName();

    /**
     * 数据库名
     * @param bool $quoted - 是否 quoted, $quoted = true 也可能返回没有 quoted 的数据库名, 具体看子类实现和是否需要, 但要保证 $quoted = false 返回没有 quoted 的数据库名
     * @return string
     */
    abstract public function getDb($quoted = true);

    /**
     * 子类可以重定义
     * 主要用于解决 数据库名 冲突的情形
     * 在项目中存在多 数据库实例 的场景下, 可能存在不同 数据库实例 中有相同的 数据库名
     * 那么在需要使用 数据库名 且 不能冲突 的情形下, 我们可以给 冲突的 数据库名 取一个别名
     * 来保证在项目中通过 BaseEntity::getDbAlias 取到的所有 数据库名 都是唯一的, 不存在冲突
     *
     * 例如: 缓存设计
     * 缓存 被设计成基于 数据库名 而不是基于 连接名 的
     * 连接名 被设计成 相同的数据库可以使用 不同 的连接名
     * 那么就可能会出现 项目中 使用 连接名 read 来读 数据库 db, 使用 连接名 read_write 来读写 数据库 db
     *
     * 如果 缓存 被设计成基于 连接名 的, 那么 对于 table(id) 这条数据来说, 就会存在 缓存 read_table(id) 和 缓存 read_write_table(id) 2 条缓存数据
     * 显然这样很浪费 缓存空间,
     * 而且 基于连接名 read 和 连接名 read_write, 会产生 2 个 Entity, ReadTableEntity(read) 和 ReadWriteTableEntity(read_write),
     * ReadTableEntity 取到 id 的数据会缓存在 read_table(id) 中, ReadWriteTableEntity 取到 id 的数据会缓存在 read_write_table(id) 中,
     * 当使用 ReadWriteTableEntity 更新 id 的数据时, 会清理 缓存 read_write_table(id), 但不会清理 缓存 read_table(id),
     * 于是使用 ReadTableEntity 读取 id 的数据时, 就会取到 缓存 read_table(id) 中的旧数据.
     *
     * 所以, 缓存 应该被设计成基于 数据库名 而不是基于 连接名 的,
     * 而且, 作为缓存键 组成部分的 数据库名 也不需要完全等于真实的 数据库名
     *
     * @return string
     */
    protected function getDbAlias()
    {
        return $this->getDb(false);
    }

    /**
     * 表名
     * @param bool $quoted - 是否 quoted, $quoted = true 也可能返回没有 quoted 的表名, 具体看子类实现和是否需要, 但要保证 $quoted = false 返回没有 quoted 的表名
     * @return string
     */
    abstract public function getTable($quoted = true);

    /**
     * 子类可以重写
     * 返回一个 column => column 的 quoted 的形式 的 map，例如: ['id' => '`id`', 'date' => '`date`']
     * @return array
     */
    public function getColumn2QuotedMap()
    {
        return array(
            'id' => '`id`'
        );
    }

    // endregion

    // region ========== Cache ==========

    /**
     * 获取 php 缓存状态(是否开启),
     *
     * 虽然是一个成员方法, 但 php 缓存状态是独立于对象的, 只和 [$this->getDbAlias()][$this->getTable(false)] 有关, 所以这个方法其实是一个代理方法.
     *
     * 如果从没使用 setPhpCache 设置过 php 缓存状态, 那么默认 getPhpCache 返回 false,
     * 如果想设定一个不同的 默认 php 缓存状态(即 true), 请子类重写这个函数
     * @return bool
     */
    public function getPhpCache()
    {
        return !empty(EntityPhpCache::$state[$this->getDbAlias()][$this->getTable(false)]);

        // $db = $this->getDbAlias();
        // $table = $this->getTable(false);
        // if (empty(EntityPhpCache::$state[$db]) || !isset(EntityPhpCache::$state[$db][$table])) {
        //     return false; // 设定 默认 php 缓存状态
        // }
        // return EntityPhpCache::$state[$db][$table];
    }

    /**
     * 开启或关闭 php 缓存,
     *
     * 虽然是一个成员方法, 但 php 缓存状态是独立于对象的, 只和 [$this->getDbAlias()][$this->getTable(false)] 有关, 所以这个方法其实是一个代理方法.
     * 即: XxxEntity, x1 = new XxxEntity(), x2 = new XxxEntity(),
     * 当通过 x1->setPhpCache(true) 开启 php 缓存后, x2->getPhpCache() 获取到的 php 缓存状态也是 true,
     * 当通过 x1->setPhpCache(false) 关闭 php 缓存后, x2->getPhpCache() 获取到的 php 缓存状态也是 false.
     *
     * php 缓存就是 php 中把数据保存起来以便复用, 以便减少数据库查询, 提升性能,
     * 尤其是在模块化结构化程序设计中, 模块A 在功能上完备, 模块B 也在功能上完备,
     * 模块A 和 模块B 的功能代码 是不会考虑对方的存在的, 各自专注自己的功能,
     * 当 模块A 和 模块B 中的功能都需要一些相同的数据时, 模块A 和 模块B 都会查询这些数据,
     * 如果一个功能需要同时使用到 模块A 和 模块B, 那么 模块A 和 模块B 都要查询一些相同的数据, 这无疑会损害性能.
     * 在方便抽取 模块A 和 模块B 中共用的功能进行封装时一般会抽取, 但有时也不便抽取, 这涉及到封装粒度, 功能完备性, 参数设计等等,
     * 例如 模块A 和 模块B 中都有 2 行相同的代码, 我们就去把这 2 行代码封装一下显然不合适, 粒度不合适(特殊情况除外),
     * 再例如 模块A 和 模块B 中都有 十几行左右的计算代码, 这些计算代码会用到前面的一堆的变量,
     * 如果我们封装这十几行左右的计算代码, 那么从功能上来讲, 不是非常完备, 而且需要用到一堆变量, 参数会有一堆, 封装不是太合适.
     * 而如果我们选择在这个功能中 开启 php 缓存, 那么 模块A 和 模块B 都要查询一些相同的数据只会真正走一次数据库查询, 提升性能.
     *
     * 但 php 中把数据保存起来又会导致 php 无法释放这些数据占用的内存,
     * 尤其是当 php 获取到大量数据用于计算, 计算完成后这些数据就不需要了, 不会再被复用到, 这时会对系统性能造成严重损害.
     *
     * 所以, 是否 开启 php 缓存 还请慎用.
     * 是用 空间换时间, 还是用 时间换空间, 需要结合具体的情况综合考虑(包括硬件情况), 以平衡时间和空间, 达到让系统整体性能最好.
     *
     * 建议 默认关闭 php 缓存, 同没有 php 缓存 时一样优化程序, 保证性能, php 缓存 只是作为优化程序的手段之一进行考虑,
     * 在需要且合适时 开启 php 缓存, 使用完了后就马上 关闭 php 缓存.
     * 当然, 这只是一个建议, 具体怎么使用要看具体情况, 只是一定要考虑好 平衡 空间和时间,
     * 以防 php 缓存大量不会再被复用到的数据, 白白浪费内存空间.
     *
     * @param bool $state - true: 开启 php 缓存, false: 关闭 php 缓存
     * @return $this
     */
    public function setPhpCache($state)
    {
        EntityPhpCache::$state[$this->getDbAlias()][$this->getTable(false)] = !empty($state);
        return $this;
    }

    /**
     * 缓存，有就启用缓存，没有就不启用缓存
     * 子类需要缓存的话请重写这个函数返回一个 EntityCacheBase
     *
     * @return null|EntityCacheBase
     */
    public function getCache()
    {
        return null;
    }

    /**
     * 子类可以重定义
     *
     * @return int - 缓存时间, 单位：秒
     */
    public function getCacheTTL()
    {
        return 300;
    }

    /**
     * 使用 id 从 这个对象的基于id的缓存 中 获取缓存的数据
     * $id 可以传递数组
     *
     * @param int|int[]|string|string[] $id
     * @return null|array - $id 为 empty 时 return null; $id 为 array 时 return array; $id 为 int|string 时 return null|单个缓存数据;
     */
    public function getCachedValueById($id)
    {
        if (empty($id)) {
            return null;
        }

        $db = $this->getDbAlias();
        $table = $this->getTable(false);

        if (is_array($id)) {

            $ret = array();

            // 从 EntityPhpCache 中取数据
            if (!empty(EntityPhpCache::$kv[$db][$table])) {
                $ids = array();
                foreach ($id as $i) {
                    if (empty(EntityPhpCache::$kv[$db][$table][$i])) {
                        $ids[] = $i;
                    } else {
                        $ret[] = EntityPhpCache::$kv[$db][$table][$i];
                    }
                }
                if (empty($ids)) {
                    return $ret;
                }
                $id = $ids;
            }

            // 从 cache 中取数据
            $cache = $this->getCache();
            if ($cache) {
                $phpCache = $this->getPhpCache();
                foreach ($id as $i) {
                    $cachedValue = $cache->getById($db, $table, $i);
                    $cachedValue = empty($cachedValue) ? null : @ json_decode($cachedValue, true);
                    if ($cachedValue) {
                        $ret[] = $cachedValue;
                        if ($phpCache) {
                            EntityPhpCache::$kv[$db][$table][$i] = $cachedValue;
                        }
                    }
                }
            }

            return $ret;

        } else {

            if (!empty(EntityPhpCache::$kv[$db][$table][$id])) {
                return EntityPhpCache::$kv[$db][$table][$id];
            }

            $cache = $this->getCache();
            if ($cache) {
                $cachedValue = $cache->getById($db, $table, $id);
                $cachedValue = empty($cachedValue) ? null : @ json_decode($cachedValue, true);
                if ($cachedValue) {
                    if ($this->getPhpCache()) {
                        EntityPhpCache::$kv[$db][$table][$id] = $cachedValue;
                    }
                    return $cachedValue;
                }
            }

            return null;
        }
    }

    /**
     * 存 数据 到 这个对象的基于id的缓存 中
     * @param array $value
     */
    public function setIdCacheValue($value)
    {
        if (empty($value['id'])) {
            return;
        }

        $db = $this->getDbAlias();
        $table = $this->getTable(false);
        if ($this->getPhpCache()) {
            EntityPhpCache::$kv[$db][$table][$value['id']] = $value;
        }
        $cache = $this->getCache();
        if ($cache) {
            $cache->setById($db, $table, $value['id'], $this->getCacheTTL(), json_encode($value));
        }
    }

    /**
     * 存 多个数据 到 这个对象的基于id的缓存 中
     * @param array $values
     */
    public function setIdCacheValues($values)
    {
        if (empty($values)) {
            return;
        }

        $validValues = array();
        foreach ($values as $value) {
            if (!empty($value['id'])) {
                $validValues[] = $value;
            }
        }
        if (empty($validValues)) {
            return;
        }

        $db = $this->getDbAlias();
        $table = $this->getTable(false);

        if ($this->getPhpCache()) {
            foreach ($validValues as $value) {
                EntityPhpCache::$kv[$db][$table][$value['id']] = $value;
            }
        }

        $cache = $this->getCache();
        if ($cache) {
            $cacheTTL = $this->getCacheTTL();
            foreach ($validValues as $value) {
                $cache->setById($db, $table, $value['id'], $cacheTTL, json_encode($value));
            }
        }
    }

    /**
     * 清理一下这个对象的基于id的缓存
     * $id 传递了就会覆盖本 entity 实例的 id, 即参数 $id 的优先级更高
     * $id 可以传递数组
     *
     * @param null|int|int[]|string|string[] $id - 设计 以 CRUD 方式使用 Entity 时, 需要传递 $id, 以 ORM 方式使用 Entity 时, 不用传递 $id
     */
    public function clearIdCache($id = null)
    {
        if (empty($id)) {
            $id = $this->getId();
        } elseif (is_array($id)) {
            $id = $this->validIds($id);
        } else {
            $id = $this->validId($id);
        }
        if (empty($id)) {
            return;
        }

        $db = $this->getDbAlias();
        $table = $this->getTable(false);

        if (is_array($id)) {
            if (!empty(EntityPhpCache::$kv[$db][$table])) {
                foreach ($id as $i) {
                    if (isset(EntityPhpCache::$kv[$db][$table][$i])) {
                        EntityPhpCache::$kv[$db][$table][$i] = null;
                        unset(EntityPhpCache::$kv[$db][$table][$i]);
                    }
                }
            }
        } else {
            if (!empty(EntityPhpCache::$kv[$db][$table]) && isset(EntityPhpCache::$kv[$db][$table][$id])) {
                EntityPhpCache::$kv[$db][$table][$id] = null;
                unset(EntityPhpCache::$kv[$db][$table][$id]);
            }
        }

        $cache = $this->getCache();
        if ($cache) {
            $cache->delById($db, $table, $id);
        }
    }

    // 追踪 xxx 的 change log, 清理一下这个对象的基于xxx的缓存时就需要用到
    // /** @var array */
    // private $xxxChangeLog;
    //
    // /**
    //  * @param string $xxx
    //  */
    // public function setXxx($xxx)
    // {
    //     if (empty($this->xxxChangeLog)) {
    //         $this->xxxChangeLog = array();
    //     }
    //
    //     $oldXxx = $this->get_('xxx');
    //     if ($oldXxx) {
    //         $this->xxxChangeLog[$oldXxx] = 1;
    //     }
    //
    //     $xxx = $this->validXxx($xxx);
    //     if ($xxx) {
    //         $this->xxxChangeLog[$xxx] = 1;
    //     } else {
    //         $xxx = '';
    //     }
    //
    //     $this->set_('xxx', $xxx, self::DataTypeString);
    // }

    // 清理一下这个对象的其它缓存 的例子
    // /**
    //  * 清理一下这个对象的基于xxx的缓存
    //  * $xxx 传递了就会覆盖本 entity 实例的 xxx, 即参数 $xxx 的优先级更高
    //  * $xxx 可以传递数组
    //  *
    //  * @param null|int|int[]|string|string[] $xxx - 设计 以 CRUD 方式使用 Entity 时, 需要传递 $xxx, 以 ORM 方式使用 Entity 时, 不用传递 $xxx
    //  */
    // public function clearXxxCache($xxx = null)
    // {
    //     $clearChangeLog = false;
    //     if (empty($xxx)) {
    //         $xxx = $this->get_('xxx');
    //         if ($this->xxxChangeLog) {
    //             $clearChangeLog = true;
    //             if ($xxx) {
    //                 $this->xxxChangeLog[$xxx] = 1;
    //             }
    //             $xxx = array_keys($this->xxxChangeLog);
    //         }
    //     } elseif (is_array($xxx)) {
    //         $xxx = $this->validXxxs($xxx);
    //     } else {
    //         $xxx = $this->validXxx($xxx);
    //     }
    //     if (empty($xxx)) {
    //         return;
    //     }
    //
    //     $db = $this->getDbAlias();
    //     $table = $this->getTable(false);
    //
    //     // 有 php 缓存的话就要清理 php 缓存
    //
    //     $cache = $this->getCache();
    //     if ($cache) {
    //         $cache->del($db, $table, 'xxx', $xxx);
    //     }
    //
    //     if ($clearChangeLog) {
    //         $this->xxxChangeLog = null;
    //     }
    // }

    /**
     * 清理一下这个 [[ 表 ]] 的所有缓存
     * 设计 无论 以 CRUD 还是 以 ORM 方式使用 Entity 时, 都可以调用 本函数, 即 本函数 不限制 Entity 的使用方式
     */
    public function clearTableCache()
    {
        $db = $this->getDbAlias();
        $table = $this->getTable(false);
        EntityPhpCache::$kv[$db][$table] = null;
        unset(EntityPhpCache::$kv[$db][$table]);

        $cache = $this->getCache();
        if ($cache) {
            $cache->del($db, $table, '*');
        }
    }

    /**
     * 清理一下这个对象的所有缓存
     * 设计 当 以 ORM 方式使用 Entity 时, 才可以调用 本函数
     *
     * 如果子类实现了一些不同形式的缓存逻辑，则子类需要重写这个函数
     * 子类有可能实现了一些不同形式的缓存逻辑，当这个对象更新时，需要清理这个对象的所有缓存，这样才能保证所有缓存中的数据不是过时的
     * 将清理这个对象所有缓存的逻辑集中到一个函数中来，避免了在需要清理这个对象所有缓存时发生遗漏导致的某些缓存没有被清理
     *
     * 如果子类实现了其它形式的缓存逻辑, 这些其它形式的缓存逻辑一般是基于其它字段的, 为了方便说明, 我们就叫这些字段为 缓存键字段 .
     * 当调用这个函数清理这个对象的所有缓存时, 只有这个函数感知到了这些 缓存键字段, 这个函数才能清理 基于 缓存键字段 的相应的缓存 .
     * 如果这个函数无法感知到某个特定的 缓存键字段, 自然也就无法清理 基于这个 缓存键字段 的相应的缓存了 .
     *
     * 例如: 如果子类实现了基于 phone 的缓存, 那么当调用这个函数清理这个对象的所有缓存时,
     * 如果在这个函数中能获取到 phone 字段, 那么这个函数就可以清理 基于 phone 的缓存,
     * 而如果在这个函数中不能获取到 phone 字段, 那么这个函数自然也就无法清理 基于 phone 的缓存 .
     */
    public function clearCache()
    {
        $this->clearIdCache();
        // $this->clearXxxCache();
    }

    // endregion

    // region ========== CRUD ==========

    /**
     * INSERT 不用关注缓存, 直接 INSERT 到数据库
     * 使用 qb->insert($this->getQb()->insert) 来执行 INSERT
     *
     * @param array $data - 参见 QueryBuilder::insert 中的 $data 参数
     * @return false|string - 失败返回 false, 成功返回 插入数据的id
     */
    public function insert($data)
    {
        $this->qbResult = null;
        $qbResult = $this->getQb()->insert($this->getTable(), $data);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return '';
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }

        return $qbResult->getPdo()->lastInsertId();
    }

    /**
     * INSERT 不用关注缓存, 直接 INSERT 到数据库
     *
     * 使用 qb->insertMultiValue($this->getQb()->insertMultiValue) 来执行 INSERT,
     * 所以参数请参见 QueryBuilder::insertMultiValue 中的同名参数
     *
     * 当 $data 参数传入的数量太大时, 实现会将其切成一段段的来执行 INSERT, 所以不用担心传入的 $data 数量太大 .
     * 在实现将 $data 切成一段段的来执行 INSERT 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性 .
     * 如果在使用过程中发现 sql 长度太大导致错误, 请不要轻易修改本函数中切割的段大小, 请先尝试改大一些当前 sql 会话允许的最大 sql 长度,
     * 因为本函数中切割的段大小已经调整为适合大多数场景, 所以就不要轻易的调来调去, 除非确实有充分和必要的理由
     *
     * @param string[] $columns
     * @param array $data
     * @return bool - 失败返回 false, 成功返回 true
     */
    public function insertMultiValue($columns, $data)
    {
        $this->qbResult = null;

        if (empty($columns) || empty($data)) {
            return true;
        }
        if (!is_array($columns) || !is_array($data)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        $columnsCnt = count($columns);
        foreach ($data as $line) {
            if (empty($line) || !is_array($line) || count($line) < $columnsCnt) {
                throw new \InvalidArgumentException('参数传递错误');
            }
        }

        $columns = array_map(function ($column) {
            return trim(strval($column));
        }, $columns);

        $dataSize = count($data);
        $chunkSizeConst = 1000;
        if ($dataSize <= $chunkSizeConst) {
            if ($dataSize == 1) {
                $line = array_values($data[0]);
                $dataMap = array();
                for ($i = 0; $i < $columnsCnt; $i++) {
                    $dataMap[$columns[$i]] = $line[$i];
                }
                $qbResult = $this->getQb()->insert($this->getTable(), $dataMap);
            } else {
                $qbResult = $this->getQb()->insertMultiValue($this->getTable(), $columns, $data);
            }
            $this->qbResult = $qbResult;
            if (empty($qbResult)) {
                return true;
            }
            return $qbResult->getIsOk();

        } else {
            $chunks = array_chunk($data, $chunkSizeConst);
            $pdoStatement = null;
            foreach ($chunks as $chunk) {
                $chunkSize = count($chunk);
                if ($chunkSize == $chunkSizeConst) {
                    if (empty($pdoStatement)) {
                        $qbResult = $this->getQb()->insertMultiValue($this->getTable(), $columns, $chunk);
                        $this->qbResult = $qbResult;
                        if (empty($qbResult)) {
                            continue;
                        }
                        $pdoStatement = $qbResult->getPdoStatement();
                        $isOk = $qbResult->getIsOk();
                    } else {
                        $sqlParams = array();
                        foreach ($chunk as $line) {
                            $line = array_values($line);
                            for ($i = 0; $i < $columnsCnt; $i++) {
                                $sqlParams[] = $line[$i];
                            }
                        }
                        $isOk = $pdoStatement->execute($sqlParams);
                    }
                    if (empty($isOk)) {
                        return false;
                    }

                } else {
                    if ($chunkSize == 1) {
                        $line = array_values($chunk[0]);
                        $dataMap = array();
                        for ($i = 0; $i < $columnsCnt; $i++) {
                            $dataMap[$columns[$i]] = $line[$i];
                        }
                        $qbResult = $this->getQb()->insert($this->getTable(), $dataMap);
                    } else {
                        $qbResult = $this->getQb()->insertMultiValue($this->getTable(), $columns, $chunk);
                    }
                    $this->qbResult = $qbResult;
                    if (empty($qbResult)) {
                        continue;
                    }
                    if (!$qbResult->getIsOk()) {
                        return false;
                    }
                }
            }
            return true;
        }
    }

    /**
     * INSERT 不用关注缓存, 直接 INSERT 到数据库
     *
     * 重载 insertMultiValue,
     * 由于 php 没有重载机制, 所以换了一个函数名, 其目的和 insertMultiValue 相同, 只是参数不同,
     *
     * 使用 qb->insertMultiValue($this->getQb()->insertMultiValue) 来执行 INSERT
     *
     * 当 $values 参数传入的数量太大时, 实现会将其切成一段段的来执行 INSERT, 所以不用担心传入的 $values 数量太大 .
     * 在实现将 $values 切成一段段的来执行 INSERT 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性 .
     * 如果在使用过程中发现 sql 长度太大导致错误, 请不要轻易修改本函数中切割的段大小, 请先尝试改大一些当前 sql 会话允许的最大 sql 长度,
     * 因为本函数中切割的段大小已经调整为适合大多数场景, 所以就不要轻易的调来调去, 除非确实有充分和必要的理由
     *
     * @param array $values - object array, [[column1=>val11, column2=>val12, ...], [column1=>val21, column2=>val22, ...], ...]
     * @return bool - 失败返回 false, 成功返回 true
     */
    public function insertValues($values)
    {
        $this->qbResult = null;

        if (empty($values)) {
            return true;
        }
        if (!is_array($values) || empty($values[0]) || !is_array($values[0])) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        if (count($values) == 1) {
            $qbResult = $this->getQb()->insert($this->getTable(), $values[0]);
            $this->qbResult = $qbResult;
            if (empty($qbResult)) {
                return true;
            }
            return $qbResult->getIsOk();

        } else {
            $columns = array_keys($values[0]);
            if (empty($columns)) {
                throw new \InvalidArgumentException('参数传递错误');
            }
            $data = array();
            foreach ($values as $value) {
                $newVal = array();
                foreach ($columns as $column) {
                    if (!isset($value[$column])) {
                        throw new \InvalidArgumentException('参数传递错误');
                    }
                    $newVal[] = $value[$column];
                }
                $data[] = $newVal;
            }
            return $this->insertMultiValue($columns, $data);
        }
    }

    /**
     * 如果开启了缓存，执行成功后 会清理一下这个 [[ 表 ]] 的所有缓存
     * 由于本函数内无法确定是 INSERT 还是 UPDATE, 也无法确定缓存的键, 所以只能在执行成功后 清理一下这个 [[ 表 ]] 的所有缓存.
     * 这样一来, 对缓存的误伤就很大, 所以, 请慎用这个函数.
     *
     * 使用 qb->insertOrUpdate($this->getQb()->insertOrUpdate) 来执行 INSERT,
     * 所以参数请参见 QueryBuilder::insertOrUpdate 中的同名参数
     *
     * @param array $data
     * @param string[] $uniqueKeys
     * @return bool - 失败返回 false, 成功返回 true
     */
    public function insertOrUpdate($data, $uniqueKeys)
    {
        $this->qbResult = null;
        $qbResult = $this->getQb()->insertOrUpdate($this->getTable(), $data, $uniqueKeys);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return true;
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }
        $this->clearTableCache();
        return true;
    }

    /**
     * 如果开启了缓存，执行成功后 会清理一下这个 [[ 表 ]] 的所有缓存
     * 由于本函数内无法确定是 INSERT 还是 UPDATE, 也无法确定缓存的键, 所以只能在执行成功后 清理一下这个 [[ 表 ]] 的所有缓存.
     * 这样一来, 对缓存的误伤就很大, 所以, 请慎用这个函数.
     *
     * 使用 qb->insertMultiValueOrUpdate($this->getQb()->insertMultiValueOrUpdate) 来执行 INSERT,
     * 所以参数请参见 QueryBuilder::insertMultiValueOrUpdate 中的同名参数
     *
     * 当 $data 参数传入的数量太大时, 实现会将其切成一段段的来执行 INSERT, 所以不用担心传入的 $data 数量太大 .
     * 在实现将 $data 切成一段段的来执行 INSERT 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性 .
     * 如果在使用过程中发现 sql 长度太大导致错误, 请不要轻易修改本函数中切割的段大小, 请先尝试改大一些当前 sql 会话允许的最大 sql 长度,
     * 因为本函数中切割的段大小已经调整为适合大多数场景, 所以就不要轻易的调来调去, 除非确实有充分和必要的理由
     *
     * @param string[] $columns
     * @param string[] $uniqueKeys
     * @param array $data
     * @return bool - 失败返回 false, 成功返回 true
     */
    public function insertMultiValueOrUpdate($columns, $uniqueKeys, $data)
    {
        $this->qbResult = null;

        if (empty($columns) || empty($uniqueKeys) || empty($data)) {
            return true;
        }
        if (!is_array($columns) || !is_array($uniqueKeys) || !is_array($data)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        $columnsCnt = count($columns);
        foreach ($data as $line) {
            if (empty($line) || !is_array($line) || count($line) < $columnsCnt) {
                throw new \InvalidArgumentException('参数传递错误');
            }
        }

        $columns = array_map(function ($column) {
            return trim(strval($column));
        }, $columns);

        $dataSize = count($data);
        $chunkSizeConst = 1000;
        if ($dataSize <= $chunkSizeConst) {
            if ($dataSize == 1) {
                $line = array_values($data[0]);
                $dataMap = array();
                for ($i = 0; $i < $columnsCnt; $i++) {
                    $dataMap[$columns[$i]] = $line[$i];
                }
                $qbResult = $this->getQb()->insertOrUpdate($this->getTable(), $dataMap, $uniqueKeys);
            } else {
                $qbResult = $this->getQb()->insertMultiValueOrUpdate($this->getTable(), $columns, $uniqueKeys, $data);
            }
            $this->qbResult = $qbResult;
            if (empty($qbResult)) {
                return true;
            }
            if (!$qbResult->getIsOk()) {
                return false;
            }
            $this->clearTableCache();
            return true;

        } else {
            $this->clearTableCache();

            $chunks = array_chunk($data, $chunkSizeConst);
            $pdoStatement = null;
            foreach ($chunks as $chunk) {
                $chunkSize = count($chunk);
                if ($chunkSize == $chunkSizeConst) {
                    if (empty($pdoStatement)) {
                        $qbResult = $this->getQb()->insertMultiValueOrUpdate($this->getTable(), $columns, $uniqueKeys, $chunk);
                        $this->qbResult = $qbResult;
                        if (empty($qbResult)) {
                            continue;
                        }
                        $pdoStatement = $qbResult->getPdoStatement();
                        $isOk = $qbResult->getIsOk();
                    } else {
                        $sqlParams = array();
                        foreach ($chunk as $line) {
                            $line = array_values($line);
                            for ($i = 0; $i < $columnsCnt; $i++) {
                                $sqlParams[] = $line[$i];
                            }
                        }
                        $isOk = $pdoStatement->execute($sqlParams);
                    }
                    if (empty($isOk)) {
                        return false;
                    }

                } else {
                    if ($chunkSize == 1) {
                        $line = array_values($chunk[0]);
                        $dataMap = array();
                        for ($i = 0; $i < $columnsCnt; $i++) {
                            $dataMap[$columns[$i]] = $line[$i];
                        }
                        $qbResult = $this->getQb()->insertOrUpdate($this->getTable(), $dataMap, $uniqueKeys);
                    } else {
                        $qbResult = $this->getQb()->insertMultiValueOrUpdate($this->getTable(), $columns, $uniqueKeys, $chunk);
                    }
                    $this->qbResult = $qbResult;
                    if (empty($qbResult)) {
                        continue;
                    }
                    if (!$qbResult->getIsOk()) {
                        return false;
                    }
                }
            }
            return true;
        }
    }

    /**
     * 如果开启了缓存，执行成功后 会清理一下这个 [[ 表 ]] 的所有缓存
     * 由于本函数内无法确定是 INSERT 还是 UPDATE, 也无法确定缓存的键, 所以只能在执行成功后 清理一下这个 [[ 表 ]] 的所有缓存.
     * 这样一来, 对缓存的误伤就很大, 所以, 请慎用这个函数.
     *
     * 重载 insertMultiValueOrUpdate,
     * 由于 php 没有重载机制, 所以换了一个函数名, 其目的和 insertMultiValueOrUpdate 相同, 只是参数不同,
     *
     * 使用 qb->insertMultiValueOrUpdate($this->getQb()->insertMultiValueOrUpdate) 来执行 INSERT
     *
     * 当 $values 参数传入的数量太大时, 实现会将其切成一段段的来执行 INSERT, 所以不用担心传入的 $values 数量太大 .
     * 在实现将 $values 切成一段段的来执行 INSERT 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性 .
     * 如果在使用过程中发现 sql 长度太大导致错误, 请不要轻易修改本函数中切割的段大小, 请先尝试改大一些当前 sql 会话允许的最大 sql 长度,
     * 因为本函数中切割的段大小已经调整为适合大多数场景, 所以就不要轻易的调来调去, 除非确实有充分和必要的理由
     *
     * @param array $values - object array, [[column1=>val11, column2=>val12, ...], [column1=>val21, column2=>val22, ...], ...]
     * @param string[] $uniqueKeys - 参见 QueryBuilder::insertMultiValueOrUpdate 中的 $uniqueKeys 参数
     * @return bool - 失败返回 false, 成功返回 true
     */
    public function insertValuesOrUpdate($values, $uniqueKeys)
    {
        $this->qbResult = null;

        if (empty($values) || empty($uniqueKeys)) {
            return true;
        }
        if (!is_array($values) || empty($values[0]) || !is_array($values[0])) {
            throw new \InvalidArgumentException('参数传递错误');
        }
        if (!is_array($uniqueKeys)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        if (count($values) == 1) {
            return $this->insertOrUpdate($values[0], $uniqueKeys);

        } else {
            $columns = array_keys($values[0]);
            if (empty($columns)) {
                throw new \InvalidArgumentException('参数传递错误');
            }
            $data = array();
            foreach ($values as $value) {
                $newVal = array();
                foreach ($columns as $column) {
                    if (!isset($value[$column])) {
                        throw new \InvalidArgumentException('参数传递错误');
                    }
                    $newVal[] = $value[$column];
                }
                $data[] = $newVal;
            }
            return $this->insertMultiValueOrUpdate($columns, $uniqueKeys, $data);
        }
    }

    /**
     * 不走缓存，直接从数据库取数据,
     * 如果 $columns == '*' && empty($groupBy) 时, 如果取到数据, 就调用 $this->setIdCacheValues 将取到的数据存入缓存
     *
     * 使用 qb->select($this->getQb()->select) 来执行 SELECT,
     * 所以参数请参见 QueryBuilder::select 中的同名参数,
     * 所以在 where 中使用 QueryBuilder::RAW_WHERE 时也要使用 QueryBuilder::select 中的方式来转义参数字符串,
     * 即 : $this->getQb()->getRPdo()->quote
     *
     * @param string|string[] $columns
     * @param null|array $where
     * @param null|string|string[] $groupBy
     * @param null|array $orderBy
     * @param int $limit
     * @param int $offset
     * @return null|false|array
     */
    public function select($columns, $where = null, $groupBy = null, $orderBy = null, $limit = 0, $offset = 0)
    {
        $this->qbResult = null;
        $qbResult = $this->getQb()->select($this->getTable(), $columns, $where, $groupBy, $orderBy, $limit, $offset);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return null;
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }
        $result = $qbResult->getPdoStatement()->fetchAll(\PDO::FETCH_ASSOC);
        if ($result && empty($groupBy)) {
            $columnString = $this->getQb()->getColumnString();
            if ($columnString == '*') {
                $this->setIdCacheValues($result);
            }
        }
        return $result;
    }

    /**
     * 使用 $this->select($columns, $where, null, $orderBy, 1) 来执行 SELECT,
     * 所以参数请参见 $this->select 中的同名参数,
     * 使用的 缓存存取策略 也请参见 $this->select 中的 缓存存取策略.
     *
     * @param string|string[] $columns
     * @param null|array $where
     * @param null|array $orderBy
     * @return null|false|array
     */
    public function selectRow1($columns, $where = null, $orderBy = null)
    {
        $result = $this->select($columns, $where, null, $orderBy, 1);
        return empty($result) ? $result : $result[0];
    }

    /**
     * 不走缓存，直接从数据库取数据，取到数据后就返回数据，不会放到缓存中,
     *
     * 使用 qb->select($this->getQb()->select) 来执行 SELECT,
     * 所以参数请参见 QueryBuilder::select 中的同名参数,
     * 所以在 where 中使用 QueryBuilder::RAW_WHERE 时也要使用 QueryBuilder::select 中的方式来转义参数字符串,
     * 即 : $this->getQb()->getRPdo()->quote
     *
     * @param string $column - 要获取的字段名, 'col1'
     * @param null|array $where
     * @param null|array $orderBy
     * @return null|false|mixed
     */
    public function selectRow1Column1($column, $where = null, $orderBy = null)
    {
        $this->qbResult = null;
        $qbResult = $this->getQb()->select($this->getTable(), $column, $where, null, $orderBy, 1);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return null;
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }
        $result = $qbResult->getPdoStatement()->fetchAll(\PDO::FETCH_NUM);
        return empty($result[0]) ? null : $result[0][0];
    }

    /**
     * 1. 调用 $this->getCachedValueById($id) 从缓存取数据, 如果取到数据就直接返回数据
     * 2. 调用 $this->selectRow1($columns, array('id' => $id)) 取数据并返回结果
     * 3. 调用 $this->selectRow1 时使用的 缓存存取策略 也请参见 $this->selectRow1 中的 缓存存取策略.
     *
     * @param int $id - 主键id, 要求表的主键字段名为 id
     * @param string|string[] $columns - 参见 QueryBuilder::select 中的 $columns 参数
     * @return null|false|array
     */
    public function selectById($id, $columns)
    {
        $this->qbResult = null;

        $id = $this->validId($id);
        if ($id === false) {
            return null;
        }

        $cachedValue = $this->getCachedValueById($id);
        if ($cachedValue) {
            return $cachedValue;
        }

        return $this->selectRow1($columns, array('`id`' => $id));
    }

    /**
     * 1. 如果 $inColumn == 'id' && empty($andRawWhere) 时, 调用 $this->getCachedValueById($inValues) 从缓存取数据, 如果取到全部数据就直接返回数据
     * 2. 从数据库取数据, 如果 $selectColumns == '*' 时, 如果取到数据, 就调用 $this->setIdCacheValues 将取到的数据存入缓存
     *
     * 使用 qb->select($this->getQb()->select) 来执行 SELECT,
     * $andRawWhere 是传递给 QueryBuilder::select 中的 $where 参数 的 QueryBuilder::RAW_WHERE 字段的值,
     * 所以也要使用 QueryBuilder::select 中的方式来转义参数字符串,
     * 即 : $this->getQb()->getRPdo()->quote
     *
     * 当 $inValues 参数传入的数量太大时, 实现会将其切成一段段的来执行 SELECT, 所以不用担心传入的 $inValues 数量太大导致 SELECT 太慢 .
     * 由于实现会将其切成一段段的来执行 SELECT, 所以也无法整体指定排序, 如果确实需要排序, 可以在程序中对结果进行排序
     *
     * @param string|string[] $selectColumns - 参见 QueryBuilder::select 中的 $columns 参数
     * @param string $inColumn - expr, sql 的 `expr IN (value,...)` 中的 expr, 通常就是 字段名
     * @param array $inValues - sql 的 `expr IN (value,...)` 中的 value 的数组, 通常就是 int[] 或者 string[]
     * @param null|string $andRawWhere - 传递给 QueryBuilder::select 中的 $where 参数 的 QueryBuilder::RAW_WHERE 字段的值
     * @return null|array
     */
    public function selectIn($selectColumns, $inColumn, $inValues, $andRawWhere = null)
    {
        $this->qbResult = null;

        $selColStr = empty($selectColumns) ? '' : $this->getQb()->validColumnString($selectColumns);
        if (empty($selColStr)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        $inColumn = empty($inColumn) ? '' : trim(strval($inColumn));
        if (empty($inColumn) || empty($inValues)) {
            return null;
        }

        $cachedValues = array();
        $andRawWhere = empty($andRawWhere) ? null : trim($andRawWhere);
        $inId = false;
        if ($inColumn == 'id' || $inColumn == '`id`') {
            $inValues = $this->validIds($inValues);
            if (empty($inValues)) {
                return array();
            }
            $inColumn = '`id`';
            $inId = true;

            if (empty($andRawWhere)) {
                $cachedValues = $this->getCachedValueById($inValues);
                if ($cachedValues) {
                    $inValues = array_diff($inValues, array_column($cachedValues, 'id'));
                    if (empty($inValues)) {
                        return $cachedValues;
                    }
                    $inValues = array_values($inValues);
                }
            }
        }

        $inSize = count($inValues);
        $chunkSizeConst = 100;

        $where = array($inColumn => 0);
        if ($andRawWhere) {
            $where[QueryBuilder::RAW_WHERE] = $andRawWhere;
        }
        if ($inSize <= $chunkSizeConst) {
            if ($inSize == 1) {
                $where[$inColumn] = $inValues[0];
            } else {
                $where[$inColumn] = array('in' => $inValues);
            }
            if ($inId) {
                $result = $this->select($selectColumns, $where, null, null, $inSize);
            } else {
                $result = $this->select($selectColumns, $where);
            }
            if (empty($result)) {
                return $cachedValues;
            } else {
                return array_merge($cachedValues, $result);
            }
        }

        $dbValues = array();
        $chunks = array_chunk($inValues, $chunkSizeConst);
        $pdoStatement = null;
        foreach ($chunks as $chunk) {
            $chunkSize = count($chunk);
            if ($chunkSize == $chunkSizeConst) {
                if (empty($pdoStatement)) {
                    $where[$inColumn] = array('in' => $chunk);
                    if ($inId) {
                        $qbResult = $this->getQb()->select($this->getTable(), $selectColumns, $where, null, null, $chunkSize);
                    } else {
                        $qbResult = $this->getQb()->select($this->getTable(), $selectColumns, $where);
                    }
                    $this->qbResult = $qbResult;
                    if (empty($qbResult)) {
                        continue;
                    }
                    $pdoStatement = $qbResult->getPdoStatement();
                    $isOk = $qbResult->getIsOk();
                } else {
                    $isOk = $pdoStatement->execute($chunk);
                }
                if ($isOk) {
                    $result = $pdoStatement->fetchAll(\PDO::FETCH_ASSOC);
                    if ($result) {
                        $dbValues = array_merge($dbValues, $result);
                    }
                }

            } else {
                if ($chunkSize == 1) {
                    $where[$inColumn] = $chunk[0];
                } else {
                    $where[$inColumn] = array('in' => $chunk);
                }
                if ($inId) {
                    $qbResult = $this->getQb()->select($this->getTable(), $selectColumns, $where, null, null, $chunkSize);
                } else {
                    $qbResult = $this->getQb()->select($this->getTable(), $selectColumns, $where);
                }
                $this->qbResult = $qbResult;
                if ($qbResult && $qbResult->getIsOk()) {
                    $result = $qbResult->getPdoStatement()->fetchAll(\PDO::FETCH_ASSOC);
                    if ($result) {
                        $dbValues = array_merge($dbValues, $result);
                    }
                }
            }
        }

        if (empty($dbValues)) {
            return $cachedValues;
        } else {
            if ($selColStr == '*') {
                $this->setIdCacheValues($dbValues);
            }
            return array_merge($cachedValues, $dbValues);
        }
    }

    /**
     * 使用 $this->selectIn($columns, 'id', $ids) 来执行 SELECT,
     * 所以使用的 缓存存取策略 也请参见 $this->selectIn 中的 缓存存取策略.
     *
     * @param int[] $ids - 主键id的数组, 要求表的主键字段名为 id
     * @param string|string[] $columns - 参见 QueryBuilder::select 中的 $columns 参数
     * @return null|array
     */
    public function selectByIds($ids, $columns)
    {
        return $this->selectIn($columns, '`id`', $ids);
    }

    /**
     * 如果开启了缓存，更新成功后 会清理一下这个 [[ 表 ]] 的所有缓存
     *
     * 使用 qb->update($this->getQb()->update) 来执行 UPDATE,
     * 所以参数请参见 QueryBuilder::update 中的同名参数,
     * 所以在 where 中使用 QueryBuilder::RAW_WHERE 时也要使用 QueryBuilder::update 中的方式来转义参数字符串,
     * 即 : $this->getQb()->getRwPdo()->quote
     *
     * @param array $data
     * @param null|array $where
     * @param null|array $orderBy
     * @param int $limit
     * @return false|int - 失败返回 false, 成功返回 更新的行数
     */
    public function update($data, $where = null, $orderBy = null, $limit = 0)
    {
        $this->qbResult = null;
        $qbResult = $this->getQb()->update($this->getTable(), $data, $where, $orderBy, $limit);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return 0;
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }
        $rowCount = $qbResult->getPdoStatement()->rowCount();
        $this->clearTableCache();
        return $rowCount;
    }

    /**
     * 如果开启了缓存, 执行更新前 会清理一下这个对象的基于id的缓存
     *
     * 使用 qb->updateMultiValue($this->getQb()->updateMultiValue) 来执行 UPDATE,
     * 所以参数请参见 QueryBuilder::updateMultiValue 中的同名参数
     *
     * 当 $data 参数传入的数量太大时, 实现会将其切成一段段的来执行 UPDATE, 所以不用担心传入的 $data 数量太大 .
     * 在实现将 $data 切成一段段的来执行 UPDATE 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性 .
     *
     * @param string[] $columns
     * @param array $data
     * @return false|int - 失败返回 false, 成功返回 更新的行数
     */
    public function updateMultiValue($columns, $data)
    {
        $this->qbResult = null;

        if (empty($columns) || empty($data)) {
            return 0;
        }
        if (!is_array($columns) || !is_array($data)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        $columnsCnt = count($columns);

        $validIds = array();
        foreach ($data as $id => $line) {
            if (empty($id) || empty($line) || !is_array($line) || count($line) < $columnsCnt) {
                throw new \InvalidArgumentException('参数传递错误');
            }
            $validIds[] = $id;
        }

        $this->clearIdCache($validIds);

        $columns = array_map(function ($column) {
            return trim(strval($column));
        }, $columns);

        $dataSize = count($data);
        $chunkSizeConst = 100;
        if ($dataSize <= $chunkSizeConst) {
            if ($dataSize == 1) {
                $id = null;
                $line = null;
                foreach ($data as $id => $line) {
                    break;
                }
                $line = array_values($line);

                $dataMap = array();
                for ($i = 0; $i < $columnsCnt; $i++) {
                    $dataMap[$columns[$i]] = $line[$i];
                }
                $qbResult = $this->getQb()->update($this->getTable(), $dataMap, array('`id`' => $id), null, 1);
            } else {
                $qbResult = $this->getQb()->updateMultiValue($this->getTable(), $columns, $data);
            }
            $this->qbResult = $qbResult;
            if (empty($qbResult)) {
                return 0;
            }
            if (!$qbResult->getIsOk()) {
                return false;
            }
            return $qbResult->getPdoStatement()->rowCount();

        } else {
            $chunks = array_chunk($data, $chunkSizeConst, true);
            $pdoStatement = null;
            $upCntSum = 0;
            foreach ($chunks as $chunk) {
                $chunkSize = count($chunk);
                if ($chunkSize == $chunkSizeConst) {
                    if (empty($pdoStatement)) {
                        $qbResult = $this->getQb()->updateMultiValue($this->getTable(), $columns, $chunk);
                        $this->qbResult = $qbResult;
                        if (empty($qbResult)) {
                            continue;
                        }
                        $pdoStatement = $qbResult->getPdoStatement();
                        $isOk = $qbResult->getIsOk();
                    } else {
                        $sqlParams = array();
                        foreach ($chunk as $id => $line) {
                            $sqlParams[] = $id;
                            $line = array_values($line);
                            for ($i = 0; $i < $columnsCnt; $i++) {
                                $sqlParams[] = $line[$i];
                            }
                        }
                        $isOk = $pdoStatement->execute($sqlParams);
                    }
                    if (empty($isOk)) {
                        return false;
                    }
                    $upCntSum += intval($pdoStatement->rowCount());

                } else {
                    if ($chunkSize == 1) {
                        $id = null;
                        $line = null;
                        foreach ($chunk as $id => $line) {
                            break;
                        }
                        $line = array_values($line);

                        $dataMap = array();
                        for ($i = 0; $i < $columnsCnt; $i++) {
                            $dataMap[$columns[$i]] = $line[$i];
                        }
                        $qbResult = $this->getQb()->update($this->getTable(), $dataMap, array('`id`' => $id), null, 1);
                    } else {
                        $qbResult = $this->getQb()->updateMultiValue($this->getTable(), $columns, $chunk);
                    }
                    $this->qbResult = $qbResult;
                    if (empty($qbResult)) {
                        continue;
                    }
                    if (!$qbResult->getIsOk()) {
                        return false;
                    }
                    $upCntSum += intval($qbResult->getPdoStatement()->rowCount());
                }
            }
            return $upCntSum;
        }
    }

    /**
     * 如果开启了缓存, 执行更新前 会清理一下这个对象的基于id的缓存
     *
     * 重载 updateMultiValue,
     * 由于 php 没有重载机制, 所以换了一个函数名, 其目的和 updateMultiValue 相同, 只是参数不同,
     *
     * 使用 qb->updateMultiValue($this->getQb()->updateMultiValue) 来执行 UPDATE
     *
     * 当 $values 参数传入的数量太大时, 实现会将其切成一段段的来执行 UPDATE, 所以不用担心传入的 $values 数量太大 .
     * 在实现将 $values 切成一段段的来执行 UPDATE 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性 .
     *
     * @param array $values - map[map], [id1 => [column1=>val11, column2=>val12, ...], id2 => [column1=>val21, column2=>val22, ...], ...], 要求要更新的每组数据中列都相同
     * @return false|int - 失败返回 false, 成功返回 更新的行数
     */
    public function updateValues($values)
    {
        $this->qbResult = null;

        if (empty($values)) {
            return 0;
        }
        if (!is_array($values)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        $id = null;
        $value = null;
        foreach ($values as $id => $value) {
            break;
        }
        if (empty($id) || empty($value) || !is_array($value)) {
            throw new \InvalidArgumentException('参数传递错误');
        }

        if (count($values) == 1) {
            return $this->updateById($id, $value);

        } else {
            $columns = array_keys($value);
            if (empty($columns)) {
                throw new \InvalidArgumentException('参数传递错误');
            }
            $data = array();
            foreach ($values as $id => $value) {
                $newVal = array();
                foreach ($columns as $column) {
                    if (!isset($value[$column])) {
                        throw new \InvalidArgumentException('参数传递错误');
                    }
                    $newVal[] = $value[$column];
                }
                $data[$id] = $newVal;
            }
            return $this->updateMultiValue($columns, $data);
        }
    }

    /**
     * 如果开启了缓存，更新成功后 会清理一下这个对象的基于id的缓存
     *
     * 使用 qb->update($this->getQb()->update) 来执行 UPDATE
     *
     * @param int $id - 主键id, 要求表的主键字段名为 id
     * @param array $data - 参见 QueryBuilder::update 中的 $data 参数
     * @return false|int - 失败返回 false, 成功返回 更新的行数
     */
    public function updateById($id, $data)
    {
        $this->qbResult = null;

        $id = $this->validId($id);
        if ($id === false) {
            return 0;
        }

        $qbResult = $this->getQb()->update($this->getTable(), $data, array('`id`' => $id), null, 1);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return 0;
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }

        $rowCount = $qbResult->getPdoStatement()->rowCount();
        $this->clearIdCache($id);
        return $rowCount;
    }

    /**
     * 如果 $inColumn == 'id', 执行更新前 会调用 $this->clearIdCache($inValues) 清理一下这个对象的基于id的缓存,
     * 否则 执行更新前 会调用 $this->clearTableCache 清理一下这个 [[ 表 ]] 的所有缓存.
     *
     * 使用 qb->update($this->getQb()->update) 来执行 UPDATE,
     * $andRawWhere 是传递给 QueryBuilder::update 中的 $where 参数 的 QueryBuilder::RAW_WHERE 字段的值,
     * 所以也要使用 QueryBuilder::update 中的方式来转义参数字符串,
     * 即 : $this->getQb()->getRwPdo()->quote
     *
     * 当 $inValues 参数传入的数量太大时, 实现会将其切成一段段的来执行 UPDATE, 所以不用担心传入的 $inValues 数量太大导致 UPDATE 太慢 .
     * 在实现将 $inValues 切成一段段的来执行 UPDATE 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性
     *
     * @param array $data - 参见 QueryBuilder::update 中的 $data 参数
     * @param string $inColumn - expr, sql 的 `expr IN (value,...)` 中的 expr, 通常就是 字段名
     * @param array $inValues - sql 的 `expr IN (value,...)` 中的 value 的数组, 通常就是 int[] 或者 string[]
     * @param null|string $andRawWhere - 传递给 QueryBuilder::update 中的 $where 参数 的 QueryBuilder::RAW_WHERE 字段的值
     * @return false|int - 失败返回 false, 成功返回 更新的行数
     */
    public function updateIn($data, $inColumn, $inValues, $andRawWhere = null)
    {
        $this->qbResult = null;

        // $data 的判断交给 $this->getQb()->update

        $inColumn = empty($inColumn) ? '' : trim(strval($inColumn));
        if (empty($inColumn) || empty($inValues)) {
            return 0;
        }

        $inId = false;
        if ($inColumn == 'id' || $inColumn == '`id`') {
            $inValues = $this->validIds($inValues);
            if (empty($inValues)) {
                return 0;
            }
            $inColumn = '`id`';
            $inId = true;
        }

        if ($inId) {
            $this->clearIdCache($inValues);
        } else {
            $this->clearTableCache();
        }

        $inSize = count($inValues);
        $chunkSizeConst = 100;

        $where = array($inColumn => 0);
        $andRawWhere = empty($andRawWhere) ? null : trim($andRawWhere);
        if ($andRawWhere) {
            $where[QueryBuilder::RAW_WHERE] = $andRawWhere;
        }
        if ($inSize <= $chunkSizeConst) {
            if ($inSize == 1) {
                $where[$inColumn] = $inValues[0];
            } else {
                $where[$inColumn] = array('in' => $inValues);
            }
            if ($inId) {
                $qbResult = $this->getQb()->update($this->getTable(), $data, $where, null, $inSize);
            } else {
                $qbResult = $this->getQb()->update($this->getTable(), $data, $where);
            }
            $this->qbResult = $qbResult;
            if (empty($qbResult)) {
                return 0;
            }
            if (!$qbResult->getIsOk()) {
                return false;
            }
            return $qbResult->getPdoStatement()->rowCount();
        }

        $chunks = array_chunk($inValues, $chunkSizeConst);
        $pdoStatement = null;
        $dataParams = array();
        $upCntSum = 0;
        foreach ($chunks as $chunk) {
            $chunkSize = count($chunk);
            if ($chunkSize == $chunkSizeConst) {
                if (empty($pdoStatement)) {
                    $where[$inColumn] = array('in' => $chunk);
                    if ($inId) {
                        $qbResult = $this->getQb()->update($this->getTable(), $data, $where, null, $chunkSize);
                    } else {
                        $qbResult = $this->getQb()->update($this->getTable(), $data, $where);
                    }
                    $this->qbResult = $qbResult;
                    if (empty($qbResult)) {
                        continue;
                    }
                    $pdoStatement = $qbResult->getPdoStatement();
                    $sqlParams = $qbResult->getSqlParams();
                    $dataParamsLen = count($sqlParams) - $chunkSize;
                    if ($dataParamsLen > 0) {
                        $dataParams = array_splice($sqlParams, 0, $dataParamsLen);
                    }
                    $isOk = $qbResult->getIsOk();
                } else {
                    if (empty($dataParams)) {
                        $isOk = $pdoStatement->execute($chunk);
                    } else {
                        $isOk = $pdoStatement->execute(array_merge($dataParams, $chunk));
                    }
                }
                if (empty($isOk)) {
                    return false;
                }
                $upCntSum += intval($pdoStatement->rowCount());

            } else {
                if ($chunkSize == 1) {
                    $where[$inColumn] = $chunk[0];
                } else {
                    $where[$inColumn] = array('in' => $chunk);
                }
                if ($inId) {
                    $qbResult = $this->getQb()->update($this->getTable(), $data, $where, null, $chunkSize);
                } else {
                    $qbResult = $this->getQb()->update($this->getTable(), $data, $where);
                }
                $this->qbResult = $qbResult;
                if (empty($qbResult)) {
                    continue;
                }
                if (!$qbResult->getIsOk()) {
                    return false;
                }
                $upCntSum += intval($qbResult->getPdoStatement()->rowCount());
            }
        }
        return $upCntSum;
    }

    /**
     * 执行更新前 会调用 $this->clearIdCache($ids) 清理一下这个对象的基于id的缓存
     *
     * 使用 $this->updateIn($data, 'id', $ids) 来执行 UPDATE
     *
     * @param int[] $ids - 主键id的数组, 要求表的主键字段名为 id
     * @param array $data - 参见 QueryBuilder::update 中的 $data 参数
     * @return false|int - 失败返回 false, 成功返回 更新的行数
     */
    public function updateByIds($ids, $data)
    {
        return $this->updateIn($data, '`id`', $ids);
    }

    /**
     * 如果开启了缓存，删除成功后 会清理一下这个 [[ 表 ]] 的所有缓存
     *
     * 使用 qb->delete($this->getQb()->delete) 来执行 DELETE,
     * 所以参数请参见 QueryBuilder::delete 中的同名参数,
     * 所以在 where 中使用 QueryBuilder::RAW_WHERE 时也要使用 QueryBuilder::delete 中的方式来转义参数字符串,
     * 即 : $this->getQb()->getRwPdo()->quote
     *
     * @param null|array $where
     * @param null|array $orderBy
     * @param int $limit
     * @return false|int - 失败返回 false, 成功返回 删除的行数
     */
    public function delete($where = null, $orderBy = null, $limit = 0)
    {
        $this->qbResult = null;
        $qbResult = $this->getQb()->delete($this->getTable(), $where, $orderBy, $limit);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return 0;
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }
        $rowCount = $qbResult->getPdoStatement()->rowCount();
        $this->clearTableCache();
        return $rowCount;
    }

    /**
     * 如果开启了缓存，删除成功后 会清理一下这个对象的基于id的缓存
     *
     * 使用 qb->delete($this->getQb()->delete) 来执行 DELETE
     *
     * @param int $id - 主键id, 要求表的主键字段名为 id
     * @return false|int - 失败返回 false, 成功返回 删除的行数
     */
    public function deleteById($id)
    {
        $this->qbResult = null;

        $id = $this->validId($id);
        if ($id === false) {
            return 0;
        }

        $qbResult = $this->getQb()->delete($this->getTable(), array('`id`' => $id), null, 1);
        $this->qbResult = $qbResult;
        if (empty($qbResult)) {
            return 0;
        }
        if (!$qbResult->getIsOk()) {
            return false;
        }

        $rowCount = $qbResult->getPdoStatement()->rowCount();
        $this->clearIdCache($id);
        return $rowCount;
    }

    /**
     * 如果 $inColumn == 'id', 执行更新前 会调用 $this->clearIdCache($inValues) 清理一下这个对象的基于id的缓存,
     * 否则 执行更新前 会调用 $this->clearTableCache 清理一下这个 [[ 表 ]] 的所有缓存.
     *
     * 使用 qb->delete($this->getQb()->delete) 来执行 DELETE,
     * $andRawWhere 是传递给 QueryBuilder::delete 中的 $where 参数 的 QueryBuilder::RAW_WHERE 字段的值,
     * 所以也要使用 QueryBuilder::delete 中的方式来转义参数字符串,
     * 即 : $this->getQb()->getRwPdo()->quote
     *
     * 当 $inValues 参数传入的数量太大时, 实现会将其切成一段段的来执行 DELETE, 所以不用担心传入的 $inValues 数量太大导致 DELETE 太慢 .
     * 在实现将 $inValues 切成一段段的来执行 DELETE 时, 只要遇到 sql 执行失败就返回 false, 以保证整个函数在事务中行为的正确性
     *
     * @param string $inColumn - expr, sql 的 `expr IN (value,...)` 中的 expr, 通常就是 字段名
     * @param array $inValues - sql 的 `expr IN (value,...)` 中的 value 的数组, 通常就是 int[] 或者 string[]
     * @param null|string $andRawWhere - 传递给 QueryBuilder::delete 中的 $where 参数 的 QueryBuilder::RAW_WHERE 字段的值
     * @return false|int - 失败返回 false, 成功返回 删除的行数
     */
    public function deleteIn($inColumn, $inValues, $andRawWhere = null)
    {
        $this->qbResult = null;

        $inColumn = empty($inColumn) ? '' : trim(strval($inColumn));
        if (empty($inColumn) || empty($inValues)) {
            return 0;
        }

        $inId = false;
        if ($inColumn == 'id' || $inColumn == '`id`') {
            $inValues = $this->validIds($inValues);
            if (empty($inValues)) {
                return 0;
            }
            $inColumn = '`id`';
            $inId = true;
        }

        if ($inId) {
            $this->clearIdCache($inValues);
        } else {
            $this->clearTableCache();
        }

        $inSize = count($inValues);
        $chunkSizeConst = 100;

        $where = array($inColumn => 0);
        $andRawWhere = empty($andRawWhere) ? null : trim($andRawWhere);
        if ($andRawWhere) {
            $where[QueryBuilder::RAW_WHERE] = $andRawWhere;
        }
        if ($inSize <= $chunkSizeConst) {
            if ($inSize == 1) {
                $where[$inColumn] = $inValues[0];
            } else {
                $where[$inColumn] = array('in' => $inValues);
            }
            if ($inId) {
                $qbResult = $this->getQb()->delete($this->getTable(), $where, null, $inSize);
            } else {
                $qbResult = $this->getQb()->delete($this->getTable(), $where);
            }
            $this->qbResult = $qbResult;
            if (empty($qbResult)) {
                return 0;
            }
            if (!$qbResult->getIsOk()) {
                return false;
            }
            return $qbResult->getPdoStatement()->rowCount();
        }

        $chunks = array_chunk($inValues, $chunkSizeConst);
        $pdoStatement = null;
        $delCntSum = 0;
        foreach ($chunks as $chunk) {
            $chunkSize = count($chunk);
            if ($chunkSize == $chunkSizeConst) {
                if (empty($pdoStatement)) {
                    $where[$inColumn] = array('in' => $chunk);
                    if ($inId) {
                        $qbResult = $this->getQb()->delete($this->getTable(), $where, null, $chunkSize);
                    } else {
                        $qbResult = $this->getQb()->delete($this->getTable(), $where);
                    }
                    $this->qbResult = $qbResult;
                    if (empty($qbResult)) {
                        continue;
                    }
                    $pdoStatement = $qbResult->getPdoStatement();
                    $isOk = $qbResult->getIsOk();
                } else {
                    $isOk = $pdoStatement->execute($chunk);
                }
                if (empty($isOk)) {
                    return false;
                }
                $delCntSum += intval($pdoStatement->rowCount());

            } else {
                if ($chunkSize == 1) {
                    $where[$inColumn] = $chunk[0];
                } else {
                    $where[$inColumn] = array('in' => $chunk);
                }
                if ($inId) {
                    $qbResult = $this->getQb()->delete($this->getTable(), $where, null, $chunkSize);
                } else {
                    $qbResult = $this->getQb()->delete($this->getTable(), $where);
                }
                $this->qbResult = $qbResult;
                if (empty($qbResult)) {
                    continue;
                }
                if (!$qbResult->getIsOk()) {
                    return false;
                }
                $delCntSum += intval($qbResult->getPdoStatement()->rowCount());
            }
        }
        return $delCntSum;
    }

    /**
     * 执行更新前 会调用 $this->clearIdCache($ids) 清理一下这个对象的基于id的缓存
     *
     * 使用 $this->deleteIn('id', $ids) 来执行 DELETE
     *
     * @param int[] $ids - 主键id的数组, 要求表的主键字段名为 id
     * @return false|int - 失败返回 false, 成功返回 删除的行数
     */
    public function deleteByIds($ids)
    {
        return $this->deleteIn('`id`', $ids);
    }

    // endregion

    // region ========== ORM ==========

    /**
     * @return array
     */
    public function getData()
    {
        return $this->data;
    }

    /**
     * @param string $key
     * @return mixed
     */
    protected function get_($key)
    {
        if (empty($this->data) || !isset($this->data[$key])) {
            return null;
        }

        return $this->data[$key];
    }

    /**
     * @param string $key
     * @param mixed $value
     * @param int $dataType - refer self::DataType**
     * @param bool $isAllowNull - 是否允许NULL
     * @param bool $monitor - 是否监视值的改变,例如使用sql改变了数据库的值,然后需要同步到这个对象来,这时就需要关闭监视
     * @param int $floatPrecision - float精度,只影响 self::DataTypeFloat
     * @return $this
     */
    protected function set_($key, $value, $dataType, $isAllowNull = false, $monitor = true, $floatPrecision = 2)
    {
        if (empty($this->data)) {
            $this->data = array();
        }
        if (!$monitor) {
            $this->data[$key] = $value;

            return $this;
        }

        if (empty($this->changeLog)) {
            $this->changeLog = array();
        }

        if (isset($this->data[$key])) {
            $isDiff = false;
            $isNeedDoDiff = false;
            if ($isAllowNull) {
                if (isset($value)) {
                    $isNeedDoDiff = true;
                } else {
                    $isDiff = true;
                }
            } else {
                $isNeedDoDiff = true;
            }

            if ($isNeedDoDiff) {
                $oldVal = $this->data[$key];
                if ($dataType == self::DataTypeBool) {
                    $oldVal = empty($oldVal) ? 0 : 1;
                    $value = empty($value) ? 0 : 1;
                    if ($value !== $oldVal) {
                        $isDiff = true;
                    }
                } elseif ($dataType == self::DataTypeInt) {
                    $oldVal = empty($oldVal) ? 0 : intval($oldVal);
                    $value = empty($value) ? 0 : intval($value);
                    if ($value !== $oldVal) {
                        $isDiff = true;
                    }
                } elseif ($dataType == self::DataTypeFloat) {
                    $oldVal = empty($oldVal) ? 0 : floatval($oldVal);
                    $value = empty($value) ? 0 : floatval($value);
                    $diff = $value - $oldVal;
                    if ($floatPrecision < 1) {
                        $floatPrecision = 0;
                    }
                    $boundary = pow(10, -($floatPrecision + 1));
                    if ($diff > $boundary || $diff < -$boundary) {
                        $isDiff = true;
                    }
                } elseif ($dataType == self::DataTypeString) {
                    $oldVal = empty($oldVal) ? '' : strval($oldVal);
                    $value = empty($value) ? '' : strval($value);
                    if ($value !== $oldVal) {
                        $isDiff = true;
                    }
                } else {
                    if ($value !== $oldVal) {
                        $isDiff = true;
                    }
                }
            }
            if ($isDiff) {
                $this->data[$key] = $value;
                $this->changeLog[$key] = $value;
            }
        } else {
            $isDiff = false;
            $isNeedDoFix = false;
            if ($isAllowNull) {
                if (isset($value)) {
                    $isDiff = true;
                    $isNeedDoFix = true;
                }
            } else {
                $isDiff = true;
                $isNeedDoFix = true;
            }

            if ($isNeedDoFix) {
                if ($dataType == self::DataTypeBool) {
                    $value = empty($value) ? 0 : 1;
                } elseif ($dataType == self::DataTypeInt) {
                    $value = empty($value) ? 0 : intval($value);
                } elseif ($dataType == self::DataTypeFloat) {
                    $value = empty($value) ? 0 : floatval($value);
                } elseif ($dataType == self::DataTypeString) {
                    $value = empty($value) ? '' : strval($value);
                }
            }
            if ($isDiff) {
                $this->data[$key] = $value;
                $this->changeLog[$key] = $value;
            }
        }
        return $this;
    }

    /**
     * @return int
     */
    public function getId()
    {
        return intval($this->get_('id'));
    }

    /**
     * 主要用于格式化返回到客户端的数据
     * 主要功能是做key的映射，data 中的 key = x 时，可能要求返回给客户端的 key = y, 所以这时需要一个映射
     * 返回给客户端的key不允许首尾有空白字符
     * 如果指定返回给客户端的key位空串或者全部是空白字符，则表示不返回这个字段给客户端
     *
     * @param null|array $mapping
     * @return array|null
     */
    public function format($mapping = null)
    {
        if (empty($mapping) || empty($this->data)) {
            return $this->data;
        }

        $fData = array();
        foreach ($this->data as $key => $val) {
            if (isset($mapping[$key])) {
                $mapKey = $mapping[$key];
                $mapKey = empty($mapKey) ? null : trim($mapKey);
                if (empty($mapKey)) {
                    continue;
                }
                $fData[$mapKey] = $val;
            } else {
                $fData[$key] = $val;
            }
        }

        return $fData;
    }

    /**
     * 使用 $this->select('*', $where, null, $orderBy, $limit, $offset) 来执行 SELECT,
     * 所以参数请参见 $this->select 中的同名参数
     *
     * @param null|array $where
     * @param null|array $orderBy
     * @param int $limit
     * @param int $offset
     * @return null|false|static[]
     */
    public function selectObjects($where = null, $orderBy = null, $limit = 0, $offset = 0)
    {
        $result = $this->select('*', $where, null, $orderBy, $limit, $offset);
        if (empty($result)) {
            return $result;
        }

        $ret = array();
        $class = get_class($this);
        foreach ($result as $item) {
            $ret[] = new $class($item);
        }
        return $ret;
    }

    /**
     * 使用 $this->selectRow1('*', $where, $orderBy) 来执行 SELECT,
     * 所以参数请参见 $this->selectRow1 中的同名参数
     *
     * @param null|array $where
     * @param null|array $orderBy
     * @return null|false|static
     */
    public function selectObject($where = null, $orderBy = null)
    {
        $result = $this->selectRow1('*', $where, $orderBy);
        if (empty($result)) {
            return $result;
        }

        $class = get_class($this);
        return new $class($result);
    }

    /**
     * 使用 $this->selectById($id, '*') 来执行 SELECT
     *
     * @param int $id - 主键id, 要求表的主键字段名为 id
     * @return null|false|static
     */
    public function selectObjectById($id)
    {
        $result = $this->selectById($id, '*');
        if (empty($result)) {
            return $result;
        }

        $class = get_class($this);
        return new $class($result);
    }

    /**
     * 使用 $this->selectIn('*', $inColumn, $inValues, $andRawWhere) 来执行 SELECT,
     * 所以参数请参见 $this->selectIn 中的同名参数
     *
     * @param string $inColumn
     * @param array $inValues
     * @param null|string $andRawWhere
     * @return null|static[]
     */
    public function selectObjectsIn($inColumn, $inValues, $andRawWhere = null)
    {
        $result = $this->selectIn('*', $inColumn, $inValues, $andRawWhere);
        if (empty($result)) {
            return $result;
        }

        $ret = array();
        $class = get_class($this);
        foreach ($result as $item) {
            $ret[] = new $class($item);
        }
        return $ret;
    }

    /**
     * 使用 $this->selectByIds($ids, '*') 来执行 SELECT
     *
     * @param int[] $ids - 主键id的数组, 要求表的主键字段名为 id
     * @return null|static[]
     */
    public function selectObjectsByIds($ids)
    {
        $result = $this->selectByIds($ids, '*');
        if (empty($result)) {
            return $result;
        }

        $ret = array();
        $class = get_class($this);
        foreach ($result as $item) {
            $ret[] = new $class($item);
        }
        return $ret;
    }

    /**
     * 有个应用场景:
     * 一个事务，可能有多个 Entity 需要更新到数据库，
     * 但是如果可以判断每个 Entity 是否需要更新到数据库，那么我们就可以决定是否使用事务
     *
     * @return bool
     */
    public function isNeedUpdateToDb()
    {
        if ($this->changeLog) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 如果开启了缓存，更新成功后 会清理一下这个对象的所有缓存
     *
     * @return null|QueryBuilderResult
     */
    public function updateToDb()
    {
        $this->qbResult = null;

        if ($this->changeLog) {
            $id = $this->getId();
            if ($id && empty($this->changeLog['id'])) {
                $qbResult = $this->getQb()->update($this->getTable(), $this->quoteColumnK($this->changeLog), array('`id`' => $id), null, 1);
                $this->qbResult = $qbResult;
                if ($qbResult && $qbResult->getIsOk()) {
                    $this->changeLog = null;
                    $this->clearCache();
                }
                return $qbResult;
            } else {
                $qbResult = $this->getQb()->insert($this->getTable(), $this->quoteColumnK($this->changeLog));
                $this->qbResult = $qbResult;
                if ($qbResult && $qbResult->getIsOk()) {
                    $this->changeLog = null;
                    if (empty($id)) {
                        $id = $qbResult->getPdo()->lastInsertId();
                        $this->data['id'] = $id;
                    }
                }
                return $qbResult;
            }
        }

        return null;
    }

    /**
     * 如果开启了缓存，删除成功后 会清理一下这个对象的所有缓存
     *
     * 使用 qb->delete($this->getQb()->delete) 来执行 DELETE
     *
     * @return null|QueryBuilderResult
     */
    public function deleteFromDb()
    {
        $this->qbResult = null;

        $id = $this->getId();
        if (empty($id)) {
            return null;
        }

        $qbResult = $this->getQb()->delete($this->getTable(), array('`id`' => $id), null, 1);
        $this->qbResult = $qbResult;
        if ($qbResult && $qbResult->getIsOk()) {
            $this->clearCache();
            $this->changeLog = null;
            $this->data = null;
        }
        return $qbResult;
    }

    // endregion

}
